import math
import copy
from collections import deque
from requests import Response
from quant.accounts import UserData
from quant.accounts.api import Api, Spi
from quant.accounts.accounts import Accounts, Account
from quant.markets import Markets, functions
from quant.utils import EventEngine, data_routing_key, Iota, logging
from quant.const import SimMatchingMode, Event, UserEvent, OrderType, OrderStatus, ApiType


class SimAccounts(Accounts):
    def __init__(self, markets):
        if markets is None:
            from quant.markets import SimMarkets
            markets = SimMarkets()

        super().__init__()
        self.markets: Markets = markets
        self.simulate_setting = {
            'margin': {},
            'balance': {},
            'latency': 0,
            'matching_mode': SimMatchingMode.Normal
        }

    def set_margin(self, margin):
        self.simulate_setting['margin'] = copy.deepcopy(margin)

    def set_balance(self, balance):
        self.simulate_setting['balance'] = copy.deepcopy(balance)

    def set_latency(self, ms):
        self.simulate_setting['latency'] = ms / 1000

    def set_matching_mode(self, mode=SimMatchingMode.Normal):
        if mode not in engine_map:
            err = 'unknown matching mode {}'.format(mode)
            raise Exception(err)
        self.simulate_setting['matching_mode'] = mode

    def create_account(self, exchange, keys):
        setting = copy.deepcopy(self.simulate_setting)
        account = SimAccount(exchange, keys, self.markets, setting)
        self.all_accounts.append(account)
        return account


class SimAccount(Account):
    def __init__(self, exchange, keys, markets, setting):
        super(SimAccount, self).__init__(exchange, keys)
        self.markets = markets
        self.simulate_setting = setting

        self.balance = setting['balance']
        self.margin = setting['margin']

        self.simulate_balance = {}
        self.begin_margin = copy.deepcopy(self.margin)

        self.api = SimApi(keys, self)
        self.spi = SimSpi(keys, self)


class SimApi(Api):
    _symbol_matcher: dict
    _id_creator = None
    _added: set

    def init(self):
        self._symbol_matcher = {}  # {symbol: MatchingEngine(), }
        self._id_creator = Iota()
        self._added = set()

    def _init_balance(self, symbol=None):
        if symbol:
            iterate = (symbol,)
        else:
            iterate = self._added

        balance = self.account.balance
        for symbol in iterate:
            if '.' not in symbol:
                for asset in symbol.split('/'):
                    if asset not in balance:
                        balance[asset] = {'free': 0, 'frozen': 0}

    def _init_position(self, symbol=None):
        if symbol:
            iterate = (symbol,)
        else:
            iterate = self._added

        position = self.account.position
        for symbol in iterate:
            if '.' in symbol:
                if symbol not in position:
                    position[symbol] = {'long': 0, 'short': 0, 'position': 0}

    def add_symbol(self, symbol):
        self._added.add(symbol)

    def place_order(self, order):
        # order = order.copy()  # 已在底层copy
        client_id = self._id_creator.next()
        client_id = 'Sim{}'.format(client_id)
        order.client_id = client_id

        matcher = self._get_matcher(order.symbol)
        matcher.place_order(order)
        return client_id

    def cancel_order(self, order):
        matcher = self._get_matcher(order.symbol)
        matcher.cancel_order(order)

    def _get_matcher(self, symbol):
        try:
            return self._symbol_matcher[symbol]
        except KeyError:
            logging.info('create new matcher for {} {}'.format(self.account.exchange, symbol))
            account = self.account
            exchange = account.exchange
            markets = account.markets
            setting = account.simulate_setting
            cls = engine_map[setting['matching_mode']]
            self._symbol_matcher[symbol] = cls(exchange, symbol, markets, account, setting)
            return self._symbol_matcher[symbol]

    def query_balance(self, symbol=None):
        self._init_balance(symbol)

    def query_all_balance(self):
        self._init_balance()

    def query_margin(self, symbol=None):
        return

    def query_position(self, symbol=None):
        self._init_position(symbol)

    def query_all_position(self):
        self._init_position()

    def query_open_orders(self, symbol=None):
        pass

    def query_all_open_orders(self):
        pass

    def cancel_all(self):
        err = 'api.cancel_all() for simulate not implemented'
        raise NotImplementedError(err)

    def ignore_offset(self, ignore):
        return

    def join(self):
        return

    def _get_agent(self, symbol):
        return self

    def set_ws_mode(self, ws=True):
        return

    def set_leverage(self, symbol, leverage):
        return


class SimSpi(Spi):
    def add_symbol(self, symbol):
        pass


class MatchingEngine:
    def __init__(self, exchange, symbol, markets, account, setting):
        self.exchange = exchange
        self.symbol = symbol
        self.markets = markets
        self.account = account
        self.setting = setting
        self.latency = setting['latency']

        self._queue = deque()
        self._clock = 0
        if markets.now:
            self._clock = markets.now

        markets.matching_subscribe_clock(self.on_clock)
        markets.matching_subscribe(Event.Book, exchange, symbol, self.on_book)
        markets.matching_subscribe(Event.Trade, exchange, symbol, self.on_trade)

        self._book = None
        self._buying = []
        self._selling = []

        self._function = functions.get_func_instance(exchange)
        self.init()

    def init(self):
        pass

    def place_order(self, order):
        order = order.copy()
        order.status = OrderStatus.Placing
        self._queue.append((Place, self._clock, order))

        self.account.order_processor.process_place_operation(order)

    def cancel_order(self, order):
        order = order.copy()
        order.status = OrderStatus.Canceling
        self._queue.append((Cancel, self._clock, order))

        self.account.order_processor.process_cancel_operation(order)

    def on_clock(self, clock):
        self._clock = clock
        queue = self._queue
        process_before = clock - self.latency
        while queue:
            if queue[0][1] < process_before:
                op, _, od = queue.popleft()
                if op == Place:
                    self.process_place(od)
                elif op == Cancel:
                    self.process_cancel(od)
                else:
                    raise NotImplementedError('Simulate for {} not implemented'.format(op))
            else:
                break

    def process_place(self, order):
        order = order.copy()
        if order.order_type != OrderType.PostOnly:
            err = 'Simulate for {} order-type not implemented'.format(order.order_type)
            raise NotImplementedError(err)

        if self._book is not None:  # ############# 检查
            if order.side == 'buy':
                sell_1, _ = self._book.item('sell', 1)
                if sell_1 and order.price >= sell_1:
                    order.status = OrderStatus.PlaceFailed
            else:
                buy_1, _ = self._book.item('buy', 1)
                if buy_1 and order.price <= buy_1:
                    order.status = OrderStatus.PlaceFailed

        if order.status == OrderStatus.PlaceFailed:  # ######## 检查
            self._put_operation_fail('place_order', order, 'post only order crossed')
        else:
            order.status = OrderStatus.Pending

        order = self.account.order_processor.update_order(order)
        self._put_order_update(order)

        self._build_order_list()

    def process_cancel(self, order):  # ############## 测试
        if order.client_id not in self.account.orders:
            self._put_operation_fail('cancel_order', order, 'order already done')

        order = order.copy()
        order.status = OrderStatus.Canceled
        order = self.account.order_processor.update_order(order)
        self._put_order_update(order)

        self._build_order_list()

    def on_book(self, market_data):
        self._book = market_data.data

    def on_trade(self, market_data):  # 市场单个trade，集群trade；用户单个order，多个order。多trade撮合order；多order撮合trade。单次build()，多次on_trade()行不行。各种测试
        buying = self._buying
        selling = self._selling
        matched = set()
        account_orders = self.account.orders

        for side, price, amount in market_data.data:
            if side == 'buy':
                match_list = selling
            else:
                match_list = buying

            while amount > 0 and match_list:
                to_match = match_list[0]
                if not self._is_matched(side, price, to_match.price):
                    break

                deal_amount = _min(to_match.amount, amount)
                amount -= deal_amount
                to_match.amount -= deal_amount

                to_match.status = OrderStatus.PartFilled
                if to_match.amount == 0:
                    to_match.status = OrderStatus.FullyFilled
                    match_list.pop(0)
                matched.add(to_match)

        try:
            mid_price = self._book.get_middle()
        except AttributeError:
            mid_price = None

        for order in matched:
            deal_amount = account_orders[order.client_id].amount - order.amount
            if mid_price is None:
                mid_price = order.price
            self._function.simulate_deal(order, deal_amount, mid_price, self.account)

            order = self.account.order_processor.update_order(order)
            self._put_order_update(order)

            self.account.info_engine.put_deal(order, deal_amount)

            user_data = UserData(UserEvent.Deal, ApiType.Spi, (order, deal_amount))
            self.account.routing_engine.put_user_data(user_data)

    def _build_order_list(self):  # 这种build模式有没有逻辑bug
        orders = [o.copy() for o in self.account.orders.values() if o.status in to_match_status]
        buy_orders = [o for o in orders if o.side == 'buy']
        sell_orders = [o for o in orders if o.side == 'sell']

        self._buying = sorted(buy_orders, key=_sort_order, reverse=True)
        self._selling = sorted(sell_orders, key=_sort_order)

    def _put_order_update(self, data):
        api_type = ApiType.Api
        account = self.account
        user_data = UserData(UserEvent.Order, api_type, data)
        account.info_engine.put_user_data(user_data)
        account.routing_engine.put_user_data(user_data)

    def _put_operation_fail(self, api_name, order, error):
        resp = Response()
        resp._content = error.encode()
        self.account.info_engine.put_operation_fail(api_name, order, resp)

    def _is_matched(self, trade_side, trade_price, order_price):
        if math.isclose(trade_price, order_price):
            return True
        if trade_side == 'buy':
            return trade_price > order_price
        else:
            return trade_price < order_price


class MatchingEngine1(MatchingEngine):
    def _is_matched(self, trade_side, trade_price, order_price):
        if math.isclose(trade_price, order_price):
            return False
        if trade_side == 'buy':
            return trade_price > order_price
        else:
            return trade_price < order_price


engine_map = {
    SimMatchingMode.Normal: MatchingEngine,
    SimMatchingMode.CrossIn: MatchingEngine1,
}


def _sort_order(order):
    return order.price


def _min(a, b):
    if a < b:
        return a
    return b


Place = 'Place'
Cancel = 'Cancel'
_ost = OrderStatus
to_match_status = {_ost.Pending, _ost.Canceling, _ost.PartFilled, _ost.Modifying}

"""
因为SimAccount也创建了Api，所以api底层在生成时一定不要链接。便于后续SimAccount替换掉Api！
"""


if __name__ == '__main__':
    import time
    import keys
    from quant import SimMarkets, MarketData, Order
    from quant.markets.markets import data_routing_key
    from quant.markets.functions import set_quick_mode
    from quant.utils import set_test_mode
    set_test_mode()
    set_quick_mode()

    def try_on_trade():

        from quant.const import DataFrequency
        set_test_mode()
        functions.set_quick_mode()

        mars = SimMarkets()
        accs = SimAccounts(mars)
        accs.set_margin({'usdt': 100})
        acc = accs.create_account('Okex', null_key)

        # ee_show(acc.info_engine)
        ee_show(acc.routing_engine)

        match_engine = MatchingEngine('Okex', 'eth/usdt.swap', mars, acc, accs.simulate_setting)
        acc.orders = {
            1: Order('eth/usdt.swap', side='buy', price=2681, amount=1, client_id=1, status=OrderStatus.Pending),
            2: Order('eth/usdt.swap', side='buy', price=2683, amount=1, client_id=2, status=OrderStatus.Pending),
            3: Order('eth/usdt.swap', side='buy', price=2682, amount=1, client_id=3, status=OrderStatus.Pending),
            4: Order('eth/usdt.swap', side='sell', price=2686, amount=1, client_id=4, status=OrderStatus.Pending),
            5: Order('eth/usdt.swap', side='sell', price=2685, amount=1, client_id=5, status=OrderStatus.Pending),
            6: Order('eth/usdt.swap', side='sell', price=2687, amount=1, client_id=6, status=OrderStatus.Pending),
        }

        print('-------orderList-------')
        match_engine._build_order_list()
        print(match_engine._buying)
        print(match_engine._selling)
        print('-----------------------\n')

        trades = [
            ['sell', 2679, 0.1],
            # ['buy', 2687, 0.1],
        ]

        data = MarketData('', '', '', '', '', '', '', '', trades)
        match_engine.on_trade(data)

        print('\n-------matchResult-------')
        print('margin:', acc.margin)
        print('begin_margin:', acc.begin_margin)
        print('position', acc.position)
        print('simulate_balance:', acc.simulate_balance)

    def try_stratety():
        import time
        import threading
        from quant.markets import SimMarkets, Markets
        from quant.accounts import Order
        from quant.utils import FileRawLogger, RawReplayer, set_test_mode, SimulateInfoCollector, SimulateInfoServer
        from strategy.spread_maker import QuoteSpreadMaker
        from keys import null_key
        from utils import ee_show, show_operation

        from quant.const import DataFrequency
        set_test_mode()
        functions.set_quick_mode()

        mars = SimMarkets()
        accs = SimAccounts(mars)
        accs.set_latency(20)

        class TheSetting:
            exchange = 'Binance'
            symbol = 'doge/usdt.swap'
            maker_type = 'future'
            account_name = 'bn_1'
            spread = 1
            step_count = 4
            fair_value = 50_0000
            order_value = 20000
            least_value = 1000
            tol_ratio = 1 / 3
            query_account_interval = 2

        logger = FileRawLogger('doge_lite')
        replayer = RawReplayer(mars, logger)
        info_server = SimulateInfoServer('doge')
        collector = SimulateInfoCollector(mars)

        TheSetting.spread = 3
        TheSetting.fair_value = 50_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('sp3_50w', strategy.account)

        TheSetting.fair_value = 100_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('100w', strategy.account)

        TheSetting.fair_value = 150_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('150w', strategy.account)

        TheSetting.fair_value = 200_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('200w', strategy.account)

        TheSetting.fair_value = 300_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('300w', strategy.account)

        TheSetting.fair_value = 400_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('400w', strategy.account)

        TheSetting.fair_value = 600_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('600w', strategy.account)

        TheSetting.fair_value = 800_0000
        strategy = QuoteSpreadMaker(mars, accs, TheSetting)
        strategy.start()
        collector.add_account('800w', strategy.account)

        def collect():
            while True:
                input('enter to collect:')
                try:
                    collector.collect()
                    info_server.save()
                except Exception as err:
                    print(err)
        threading.Thread(target=collect, daemon=True).start()

        print('begin')
        replayer.start(True)
        print('finish')

        collector.collect()
        info_server.join()

    def try_sim_on_raw():
        from quant.accounts import Order
        from quant.utils import set_test_mode, EventEngine
        from quant.markets.functions import set_quick_mode
        from strategy.util import ready_to_cancel
        from utils import FeedTestRawEnv
        from keys import null_key
        set_quick_mode()
        set_test_mode()
        # EventEngine.set_show_mode()

        exchange = 'Okex'
        symbol = 'doge/usdt.swap'
        amt = 100

        env = FeedTestRawEnv('doge-ok-bt-08.24-12.00')
        # env = FeedTestRawEnv('eth-ok-bt-08.24-12.00')

        # env.markets.set_push_all()
        env.markets.add_market('Book', 'Okex', 'doge/usdt.swap')
        env.markets.add_market('Book', 'Okex', 'doge/usdt.swap')

        accounts = SimAccounts(env.markets)
        accounts.set_latency(0)

        acc = accounts.create_account(exchange, null_key)
        acc.info_engine.show_problems()

        def on_md(md):
            book = md.data
            if book is None:
                return
            order = Order(md.symbol, 'buy', book.item('buy', 1)[0], amt, order_type=OrderType.PostOnly)
            acc.api.place_order(order)
            order = Order(md.symbol, 'sell', book.item('sell', 1)[0], amt, order_type=OrderType.PostOnly)
            acc.api.place_order(order)

            for o in acc.orders.values():
                if ready_to_cancel(o):
                    acc.api.cancel_order(o)

        env.markets.subscribe_all(on_md)

        total_deal = [0, 0]
        def on_deal(order, amount):
            total_deal[0] += 1
            total_deal[1] += amount
        ee = acc.info_engine
        ee.subscribe(ee.Deal, on_deal)

        env.replayer.start()

        print('\n----------final----------')
        print('balance:', acc.balance)
        print('simulate_balance:', acc.simulate_balance)
        print('margin:', acc.margin)
        print('total deal: {} amount {} times'.format(total_deal[1], total_deal[0]))
        print('position:', acc.position)

    def try_sim_engine():
        import json
        import utils

        contract = 'BTCUSDT'
        symbol = 'btc/usdt'

        def build_trades():
            format = '{"e":"aggTrade","E":{},"a":{},"s":"{}","p":"{}","q":"{}","f":{},"l":{},"T":{},"m":{}}'
            dict = {"e": "aggTrade", "E": '', "a": '', "s": contract, "p": '', "q": '', "f": '', "l": '', "T": '', "m": ''}

            trades = [
                ['buy', 10001, 0.1],
                ['sell', 10000, 0.1],
            ]

            data = []
            i = 0
            for side, price, amount in trades:
                i += 1
                m = True if side == 'sell' else False
                d = dict.copy()
                d.update({
                    'E': i,
                    'a': i,
                    'p': str(price),
                    'q': str(amount),
                    'f': i,
                    'l': i,
                    'T': i,
                    'm': m,
                })
                d = json.dumps(d)
                data.append(d)
            return data

        mar = SimMarkets()
        mar.matching_add_market('Trade', 'Binance', symbol)

        accs = SimAccounts(mar)
        accs.set_matching_mode(SimMatchingMode.CrossIn)
        acc = accs.create_account('Binance', keys.null_key)
        acc.info_engine.show_deal()
        acc.info_engine.show_problems()

        api = acc.api
        order = Order(symbol, 'buy', 10000.00000001, 0.01, order_type=OrderType.PostOnly)
        api.place_order(order)
        order = Order(symbol, 'sell', 10000.9999999, 0.01, order_type=OrderType.PostOnly)
        api.place_order(order)

        trades = build_trades()
        for raw in trades:
            mar.feed_raw('Trade', 'Binance', symbol, 'Normal', 1, raw)

    def try_exchange():
        exchange = 'Okex'
        symbol = 'btc/usdt.swap'
        mar = SimMarkets()
        mar.matching_add_market('Trade', exchange, symbol)

        accs = SimAccounts(mar)
        accs.set_matching_mode(SimMatchingMode.Normal)
        acc = accs.create_account(exchange, keys.null_key)
        acc.info_engine.show_deal()
        acc.info_engine.show_problems()

        api = acc.api
        order = Order(symbol, 'buy', 10000, 1, order_type=OrderType.PostOnly)
        api.place_order(order)
        order = Order(symbol, 'sell', 10001, 1, order_type=OrderType.PostOnly)
        api.place_order(order)

        trades = [['buy', 10002, 1], ['sell', 10000, 1]]
        mar.clock.put('clock', 0)
        mar.clock.put('clock', 1)

        routing_key = data_routing_key('Trade', exchange, symbol)
        market_data = MarketData('Trade', exchange, symbol, 'Normal', routing_key, time.time(), None, 'id', None, trades)
        mar.routing_engine.push_data(routing_key, market_data)

        print('balance', acc.balance)
        print('margin', acc.margin)
        print('position', acc.position)

    # try_on_trade()
    # try_stratety()
    # try_sim_on_raw()
    # try_sim_engine()
    try_exchange()












